package com.jialiang.yojcodesandbox.service.impl;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.dfa.FoundWord;
import cn.hutool.dfa.WordTree;
import com.jialiang.yojcodesandbox.enums.ExecuteCodeStatusEnum;
import com.jialiang.yojcodesandbox.enums.JudgeInfoMessageEnum;
import com.jialiang.yojcodesandbox.model.ExecuteCodeRequest;
import com.jialiang.yojcodesandbox.model.ExecuteCodeResponse;
import com.jialiang.yojcodesandbox.model.ExecuteMessage;
import com.jialiang.yojcodesandbox.model.JudgeInfo;
import com.jialiang.yojcodesandbox.service.CodeSandbox;
import com.jialiang.yojcodesandbox.utils.ProcessUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StopWatch;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import static com.jialiang.yojcodesandbox.constant.CodeBlackList.JAVA_SENSITIVE_WORD_LIST;

/**
 * Java 代码沙箱模板方法：定义代码沙箱执行流程
 */
@Deprecated
@Slf4j
public abstract class JavaCodeSandboxTemplate implements CodeSandbox {
    // 代码存放的根目录
    private static final String GLOBAL_CODE_DIR_NAME = "tmpCode";

    // 用户代码文件
    private static final String GLOBAL_JAVA_CLASS_NAME = "Main.java";

    // 超时时间
    private static final long TIME_OUT = 5000L;

    /**
     * 字典树，存放黑名单
     */
    private static final WordTree WORD_TREE;

    static {
        WORD_TREE = new WordTree();
        WORD_TREE.addWords(JAVA_SENSITIVE_WORD_LIST);
    }

    /**
     * 执行代码
     * @param executeCodeRequest 代码沙箱请求数据
     * @return 沙箱响应数据
     */
    @Override
    public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {

        // 用户代码
        String code = executeCodeRequest.getCode();
        // 用例输入
        List<String> inputList = executeCodeRequest.getInputList();

        // 检测代码中是否包含敏感词
        FoundWord foundWord = WORD_TREE.matchWord(code);
        if (foundWord != null) {
            log.info("代码包含敏感词：{}", foundWord.getFoundWord());
            // 包含敏感词，返回错误信息
            return ExecuteCodeResponse
                    .builder()
                    .status(ExecuteCodeStatusEnum.ERROR.getValue())
                    .judgeInfo(new JudgeInfo(JudgeInfoMessageEnum.DANGEROUS_OPERATION.getValue(), "代码包含敏感词：" + foundWord.getFoundWord(), null, null))
                    .build();
        }

        // 第一步：把用户代码保存为文件，到项目根目录的 tmpCode 目录下
        File userCodeFile = saveCodeToFile(code);
        String userCodeParentPath = userCodeFile.getParentFile().getAbsolutePath();
        log.info("用户代码文件存放目录：{}", userCodeFile);

        // 第二步：编译代码，得到 class 文件
        ExecuteMessage compileFileExecuteMessage = compileFile(userCodeFile);
        // 代码编译错误，返回错误信息
        if (compileFileExecuteMessage.getExitCode() != 0) {
            FileUtil.del(userCodeParentPath);
            return ExecuteCodeResponse
                    .builder()
                    .status(ExecuteCodeStatusEnum.FAILED.getValue())
                    .judgeInfo(new JudgeInfo(JudgeInfoMessageEnum.COMPILE_ERROR.getValue(), compileFileExecuteMessage.getErrorMessage(), null, null))
                    .build();
        }
        log.info("代码编译结果：{}", compileFileExecuteMessage);

        // 第三步：执行代码，得到输出结果
        List<ExecuteMessage> executeMessageList = runFile(userCodeFile, inputList);
        log.info("代码执行结果：{}", executeMessageList);

        // 第四步：整理输出结果
        ExecuteCodeResponse outputResponse = getOutputResponse(executeMessageList);

        // 第五步：文件清理（默认不清理）
        boolean isDelete = false;
        boolean deleteFile = deleteFile(userCodeFile, isDelete);
        log.info("文件 " + userCodeFile.getAbsoluteFile() + " 删除" + (deleteFile ? "成功" : "失败"));

        // 返回输出结果
        return outputResponse;
    }

    /**
     * 1.把用户代码保存为文件
     * @param code 用户代码
     * @return 用户代码文件对象
     */
    public File saveCodeToFile(String code) {
        // 新建根目录，存放所有用户的代码
        // 1) 先获取项目所在的目录，yoj-code-sandbox
        String userDir = System.getProperty("user.dir");
        // 2) 定义代码存放的根目录：yoj-code-sandbox\tmpCode
        String globalCodePathName = userDir + File.separator + GLOBAL_CODE_DIR_NAME;
        // 3) 判断根目录是否存在，没有则新建
        if (!FileUtil.exist(globalCodePathName)) {
            FileUtil.mkdir(globalCodePathName);
        }

        // 1.保存用户代码文件
        // 1) 存放用户代码文件的父目录，通过 UUID 随机生成目录名，隔离存放
        String userCodeParentPath = globalCodePathName + File.separator + UUID.randomUUID();
        // 2) 定义需要将用户代码写入到的文件，包含后缀名 .java
        String userCodePath = userCodeParentPath + File.separator + GLOBAL_JAVA_CLASS_NAME;
        // 3) 将用户代码以 UTF-8 格式写入到定义好的代码文件中
        File userCodeFile = FileUtil.writeString(code, userCodePath, StandardCharsets.UTF_8);
        return userCodeFile;
    }

    /**
     * 2.编译代码，得到class文件
     * @param userCodeFile 代码执行信息
     * @return
     */
    public ExecuteMessage compileFile(File userCodeFile) {
        // 编译命令
        String compileCmd = String.format("javac -encoding utf-8 %s", userCodeFile.getAbsoluteFile());
        // 新启动进程用来执行编译命令
        Process compileProcess = null;
        ExecuteMessage executeMessage;
        try {
            compileProcess = Runtime.getRuntime().exec(compileCmd);
            executeMessage = ProcessUtils.handleProcessMessage(compileProcess, "编译");
        } catch (IOException e) {
            throw new RuntimeException("IO 异常：" + e.getMessage());
        } finally {
            assert compileProcess != null;
            compileProcess.destroy();
        }
        // 编译程序，返回信息
        return executeMessage;
    }

    /**
     * 3.执行文件，获得执行结果列表
     * @param userCodeFile 用户代码文件
     * @param inputList    输入列表
     * @return 程序执行信息列表
     */
    public List<ExecuteMessage> runFile(File userCodeFile, List<String> inputList) {
        // 用户代码文件路径
        String userCodeParentPath = userCodeFile.getParentFile().getAbsolutePath();
        List<ExecuteMessage> executeMessageList = new ArrayList<>(); // 保存输出列表
        for (String inputArgs : inputList) {
            String runCmd = String.format("java -Xmx256m -Dfile.encoding=utf-8 -cp %s Main %s", userCodeParentPath, inputArgs);
            try {
                Process runProcess = Runtime.getRuntime().exec(runCmd);
                // 超时控制：创建一个新线程，睡眠超时时间后再中断执行线程
                new Thread(() -> {
                    try {
                        Thread.sleep(TIME_OUT);
                        if(runProcess.isAlive()){
                            log.info("当前程序运行超时，已中断");
                            runProcess.destroy();
                        }
                        executeMessageList.add(
                                ExecuteMessage
                                        .builder()
                                        .exitCode(-10001)
                                        .errorMessage("超时")
                                        .time(TIME_OUT)
                                        .build()
                        );
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }).start();
                // 记录程序运行时间
                StopWatch stopWatch = new StopWatch();
                stopWatch.start();
                // 运行程序，返回信息
                ExecuteMessage executeMessage = ProcessUtils.handleProcessMessage(runProcess, "运行");
                stopWatch.stop();
                executeMessage.setTime(stopWatch.getLastTaskTimeMillis());
                // 保存每一个用例执行信息
                executeMessageList.add(executeMessage);
            } catch (Exception e) {
                throw new RuntimeException("程序执行异常：", e);
            }
        }
        return executeMessageList;
    }

    /**
     * 4.获取输出结果，返回沙箱响应数据
     * @param executeMessageList 执行信息列表
     * @return
     */
    public ExecuteCodeResponse getOutputResponse(List<ExecuteMessage> executeMessageList) {
        ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
        // 获取输出列表
        List<String> outputList = new ArrayList<>();
        // 根据用时最大值判断是否超时
        long maxTime = 0;
        for (ExecuteMessage executeMessage : executeMessageList) {
            // 有错误信息
            String errorMessage = executeMessage.getErrorMessage();
            if (StrUtil.isNotBlank(errorMessage)) {
                JudgeInfo judgeInfo = new JudgeInfo();
                judgeInfo.setMessage(JudgeInfoMessageEnum.WRONG_ANSWER.getValue());
                judgeInfo.setErrDetails(errorMessage);
                executeCodeResponse.setJudgeInfo(judgeInfo);
                // 用户提交的代码 执行中存在错误
                executeCodeResponse.setStatus(ExecuteCodeStatusEnum.FAILED.getValue());
                break;
            }
            // 添加正确信息到输出列表中
            outputList.add(executeMessage.getOutput());
            // 程序执行时间
            Long time = executeMessage.getTime();
            if (time != null) {
                // 更新最大值
                maxTime = Math.max(maxTime, time);
            }
        }
        // 正常运行完成
        if (outputList.size() == executeMessageList.size()) {
            executeCodeResponse.setStatus(ExecuteCodeStatusEnum.SUCCESS.getValue());
        }
        executeCodeResponse.setOutputList(outputList);
        JudgeInfo judgeInfo = new JudgeInfo();
        judgeInfo.setTime(maxTime);
        // 原生代码沙箱要借用第三方库来获取内存占用，非常麻烦，此处不做实现
        // judgeInfo.setMemory();
        executeCodeResponse.setJudgeInfo(judgeInfo);

        return executeCodeResponse;
    }

    /**
     * 5.文件清理
     * @param userCodeFile
     * @return
     */
    public boolean deleteFile(File userCodeFile, boolean isDelete) {
        if (!isDelete) {
            return false;
        }
        if (userCodeFile.getParentFile() != null) {
            String userCodeParentPath = userCodeFile.getParentFile().getAbsolutePath();
            boolean del = FileUtil.del(userCodeParentPath);
            return del;
        }
        return true;
    }

    /**
     * 6.获取错误响应，封装一个错误处理类
     * @param e
     * @return
     */
    private ExecuteCodeResponse getErrorResponse(Throwable e) {
        // 6.错误处理，提升程序健壮性
        return ExecuteCodeResponse
                .builder()
                .outputList(new ArrayList<>())
                .status(ExecuteCodeStatusEnum.ERROR.getValue())
                .judgeInfo(new JudgeInfo(JudgeInfoMessageEnum.SYSTEM_ERROR.getValue(), e.getMessage(), null, null))
                .build();
    }

}